package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 聊天室服务端
 */
public class Server {
    /*
        java.net.ServerSocket "总机"
        ServerSocket类似于服务端的"总机",该设备上有若干的"插座"
        每个插座可以连接一个客户端的插座，实现让多个客户端连接到同一个"总机"上

        ServerSocket有两个操作:
        1:实例化时向系统申请服务端口，客户端先通过这个端口与总机建立连接
        2:一旦一个客户端与总机建立连接，总机就会分配一个"插座"与该客户端沟通
     */
    private ServerSocket serverSocket;

    private ExecutorService threadPool;
    /*
        该集合保存所有客户端的输出流，用于广播消息使用
     */
//    private List<PrintWriter> allOut = new ArrayList<>();
    /*
        存放所有客户端的输出流
        key:该客户端的昵称
        value:对应该客户端的输出流
     */
    private Map<String,PrintWriter> allOut = new HashMap<>();

    public Server(){
        try {
            System.out.println("正在启动服务端...");
            /*
                实例化ServerSocket是要指定服务端口，客户端就是通过这个端口
                找到服务端程序的。
                该端口不能与当前计算机上其他应用程序申请的端口重复，否则会抛出
                异常:java.net.BindException: Address already in use:
             */
            serverSocket = new ServerSocket(8088);
            threadPool = Executors.newFixedThreadPool(30);
            System.out.println("服务端启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void start(){
        try {
            while(true) {
                System.out.println("等待客户端连接...");
                /*
                ServerSocket上一个重要的方法:
                Socket accept()
                该方法用于接受客户端的连接，此方法是一个阻塞方法，调用后方法会"卡住"
                开始等待客户端的连接，一旦一个客户端连接，那么此方法会立即返回一个
                Socket实例，通过这个Socket就可以与刚建立连接的客户端网络交互了
             */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了!");
                //启动一个线程负责处理该客户端交互
                ClientHandler handler = new ClientHandler(socket);
//                Thread t = new Thread(handler);
//                t.start();
                threadPool.execute(handler);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务是负责与指定的客户端进行交互
     */
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//对应客户端的IP地址

        private String nickName;//客户端的昵称
        public ClientHandler(Socket socket){
            this.socket = socket;
            //通过socket获取客户端的ip地址信息
            host = socket.getInetAddress().getHostAddress();
        }
        public void run(){
            PrintWriter pw = null;
            try {
                /*
                    Socket提供的方法:
                    InputStream getInputStream()
                    通过该Socket获取的输入流可以读取到来自对应客户端发送过来的字节数据
                 */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                //客户端发送过来的第一行字符串为昵称
                nickName = br.readLine();

                //通过socket获取输出流，用于将消息发送给该客户端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

                //将该客户端的输出流存入allOut
                //同步块指定同步监视器对象时，通常使用临界资源即可
                synchronized (allOut) {
//                    allOut.add(pw);
                    allOut.put(nickName,pw);
                }
                sendMessage(nickName+"上线了，当前在线人数:"+allOut.size());


                String message;
                /*
                    服务端调用br.readLine()读取客户端发送过来的一行字符串时可能出现异常:
                    java.net.SocketException: Connection reset
                    原因:客户端异常断开(没有进行四次挥手)
                 */
                while ((message = br.readLine()) != null) {
                    //客户端发过来的聊天信息是以"@"开始，则判定为私聊
                    if(message.startsWith("@")){
                        sendMessageToSomeone(message);
                    }else {//否则为群聊模式
                        sendMessage(nickName + "[" + host + "]说:" + message);
                        //接收方为NULL表示群聊
                        saveMessage(nickName,null,message);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //处理客户端断开连接后的操作
                //1将该客户端的输出流从共享集合中删除
                synchronized (allOut) {
//                    allOut.remove(pw);
                    allOut.remove(nickName);
                }
                sendMessage(nickName+"下线了，当前在线人数:"+allOut.size());
                //2将socket关闭，释放资源
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 将消息发送给所有客户端
         * @param message
         */
        public void sendMessage(String message){
            System.out.println(message);
            synchronized (allOut) {
                //这里要求遍历集合时与增删元素互斥，因为迭代器要求遍历过程中不可以通过集合方法增删元素
//                for (PrintWriter o : allOut) {
//                    o.println(message);
//                }
                //遍历allOut的所有value
                Collection<PrintWriter> c = allOut.values();
                for(PrintWriter o : c){
                    o.println(message);
                }
            }
        }

        /**
         * 私聊功能
         * @param message
         */
        public void sendMessageToSomeone(String message){
            /*
                私聊格式:
                @对方昵称:聊天消息


                格式验证:
                @.+:.+
             */
            if(message.matches("@.+:.+")){
                //通过私聊信息提取出对方的昵称，从聊天消息第二个字符开始截取到":"
                String toNickName = message.substring(1,message.indexOf(":"));
                //截取出要发送给对方的内容
                String toMessage = message.substring(message.indexOf(":")+1);
                //通过对方的昵称获取他的输出流
                PrintWriter pw = allOut.get(toNickName);
                if(pw==null){//如果没有获取到输出流，则说明对方昵称对应的用户不存在
                    //用户[123]不存在!
                    allOut.get(nickName).println("用户["+toNickName+"]不存在!");
                }else {
                    /*
                        范传奇要对王克晶说:晚上有空么?

                        范传奇在控制台上输入:
                        @王克晶:晚上有空么?
                         ||||| ||||||||
                           ^         ^
                       toNickname    toMessage
                        此时sendMessageToSomeone方法:
                        nickname:范传奇  当前线程对应的客户端，发消息的人
                        toNickName:王克晶   收消息的人

                        最终要向王克晶发送内容:
                        范传奇悄悄对你说:晚上有空么?
                     */
                    //张三悄悄对你说:你好!
                    pw.println(nickName+"悄悄对你说:"+toMessage);
                    //将该消息保存到数据库中
                    saveMessage(nickName,toNickName,toMessage);
                }

            }else{//私聊格式错误
                //提示该用户私聊格式错误
                allOut.get(nickName).println("私聊格式错误(@对方昵称:聊天内容)，请重新输入");
            }
        }

        /**
         * 将聊天消息保存到数据库中
         * @param fromUser  发送方昵称
         * @param toUser    接收方昵称
         * @param content   聊天消息
         */
        public void saveMessage(String fromUser,String toUser,String content){
            /*
                CREATE TABLE chatinfo(
                    id INT PRIMARY KEY AUTO_INCREMENT,  # 主键
                    content VARCHAR(255),               # 聊天内容
                    from_user VARCHAR(30),              # 发送者
                    to_user VARCHAR(30)                 # 接收者
                )
             */
            try (
                    Connection connection = DBUtil.getConnection();
            ){
                String sql = "INSERT INTO chatinfo(content,from_user,to_user) " +
                             "VALUES (?,?,?)";
                PreparedStatement ps = connection.prepareStatement(sql);
                ps.setString(1,content);
                ps.setString(2,fromUser);
                ps.setString(3,toUser);
                ps.executeUpdate();
            } catch (SQLException e) {
                e.printStackTrace();
            }

        }
    }
}
